feat(calendar): add --with-zoom / --regenerate-zoom / --remove-zoom (description-mode)#590
Conversation
Live capture against real Zoom Pro + real Google Calendar — finding to reportThanks for the fast turn on this, @mvanhorn — really appreciate the structured PR, the empirical Phase 0 framing, and the test coverage. Did the live capture exercise tonight against the real surface. Headline up front: the write fails Google's validation in our testing, and I want to share the evidence in case it's useful — but with the up-front caveat that we may be missing something you saw in Phase 0. Test setup
What works ✅The bulk of the surface works exactly as documented:
What fails ❌
Sequence: Zoom meeting created via Zoom API (success) → Calendar event POST with the addOn-shaped conferenceData (400 rejection from Google) → orphaned Zoom meeting cancelled (cleanup works correctly). Reproducible across both Google accounts — including the one with the Zoom add-on already installed and authorized. Three runs, same failure each time. Comparison: what gog generates vs. what the Zoom add-on actually producesPulled return &calendar.ConferenceData{
ConferenceId: meetingID,
ConferenceSolution: &calendar.ConferenceSolution{
Key: &calendar.ConferenceSolutionKey{Type: "addOn"},
Name: "Zoom Meeting",
IconUri: iconURI,
},
EntryPoints: []*calendar.EntryPoint{entry}, // entry has: video type, uri, label, meetingCode
}Earlier (before this PR existed), I'd captured an actual Zoom add-on-produced event via
Hypothesis for the root causeThe This would explain why the Phase 6 differential (with-vs-without add-on installed) shows no behavior change — Google's validation gates on the write payload's contents (scriptId), not on the calendar's add-on installation state. But hedging hard: this is one possible explanation, not a verified root cause. The actual Google error is the bare What might we be missing? (genuine question)A few things I'm uncertain about, listed in case any of them resolves the discrepancy:
Tier 1 surface coverage statusGiven the write fails, I can only validate the gog-side behavior, not the end-to-end success path. What I confirmed:
One small doc gap to flagThe PR's Help offeredHappy to:
Thanks again for the work on this — the engineering quality of the PR itself (test coverage, redaction, audit, atomic failure semantics) is excellent. Just want to surface what we found so you have it as you decide next steps. |
@alexisperumal's live-capture test (openclaw#590 review) showed the original conferenceData write fails Google's validation with 400 "Invalid conference data". An expanded empirical matrix (recorded in the PR description and CHANGELOG) showed: - conferenceSolution.key.type="addOn" is rejected from any non-Workspace- Marketplace OAuth client regardless of payload (scriptId, passcode, notes are incidental) - Omitting key.type makes the POST succeed but Google silently strips the entire conferenceData field on storage The only path to the native "Join with Zoom" card is registering gogcli as a Workspace Marketplace Conference Add-on (large scope, separate PR). This commit pivots --with-zoom to attach the Zoom join URL + meeting ID + passcode to the Calendar event description inside HTML-comment markers. The event renders a clickable Zoom link in every Calendar UI; --regenerate-zoom replaces the block in place; --remove-zoom strips it (and clears any legacy Zoom-shaped conferenceData for backward compatibility with events created by the Zoom for Google Workspace add-on). Trade-off: no native conference card. - internal/cmd/zoom_description.go: build / apply / remove / extract helpers for the gog-managed Zoom block - internal/cmd/calendar_build.go: Zoom branch returns nil; remove unused buildZoomConferenceData, zoomLabel, zoomConferenceIconURI - internal/cmd/calendar_edit.go: create + with-zoom + regenerate-zoom + remove-zoom rewired to operate on event.Description; conferenceVersion=1 no longer sent for the Zoom path - internal/cmd/calendar_zoom.go: extractZoomMeetingID and eventConferenceProvider read the description block first, fall back to legacy conferenceData for events written by other tools - internal/cmd/calendar_zoom_test.go: assertions updated to verify the description block shape and absence of conferenceData - docs/zoom-auth-setup.md: rewritten to explain description mode and add the Google OAuth client prerequisite cross-reference @alexisperumal flagged - CHANGELOG.md: 0.17.1 entry rewritten to describe description-mode
b954a67 to
53889e2
Compare
Live capture finding confirmed, expanded matrix, and pivot to description modeThanks @alexisperumal - your live capture was correct, and pushed me to test the architecture properly. Phase 0's claim ("the direct-write architecture works") turns out to have been a misread of read-back behaviour vs write-acceptance from a non-add-on client. Apologies for the cycles. I reproduced your 400 on the same payload, captured the rejected JSON body via a temporary debug hook, and then walked the broader matrix to figure out what Google actually validates. Posting the full table here in case it's useful for the next person who hits this surface:
The This makes the direct-write architecture unreachable from a vanilla OAuth client. The Workspace Marketplace Conference Add-on path (Apps Script project, OAuth review, Marketplace listing, real Pivot in
|
Description-mode validated end-to-end — one edge-case bug foundThanks @mvanhorn for the exhaustive matrix test and the rapid pivot, and @steipete for the merge. Re-ran the live capture against TL;DR: description-mode works as designed in 4 of 5 cases I exercised; one edge-case What works ✅
Not re-tested today (unchanged from yesterday's pass): flag-mutex matrix, One bug —
|
Summary
gog calendarcan now create, regenerate, and remove Zoom video conferences as a flag family parallel to the existing--with-meetsurface. Addsgog zoom auth setupandgog zoom auth doctorfor Zoom Server-to-Server OAuth credential storage. Closes #589.The Zoom information is attached to the Calendar event description rather than
conferenceData. The original direct-write architecture (conferenceSolution.key.type="addOn") does not work for non-Workspace-Marketplace OAuth clients; see "Architecture" below for the empirical findings.Why this matters
The Zoom for Google Workspace add-on is UI-only.
agent-plex/openclaw-zoomcovers Team Chat (different product);AIGC-Hackers/openclaw-zoom-agentjoins existing meetings (does not create or schedule). The "agent schedules a Zoom meeting and attaches it to a Google Calendar event" niche is uncovered upstream.Architecture
The PR originally claimed that
conferenceDatacould be written directly withconferenceSolution.key.type="addOn"from a vanilla OAuth client. @alexisperumal's live-capture testing against real Zoom Pro + real Google Calendar accounts disproved this. A follow-up matrix of empirical tests against a real Google Calendar account confirmed the deeper finding:key.type="addOn"+ name + iconUri + entryPointsparameters.addOnParameters.parameters.scriptId(any value, including AKfycb-shaped)entryPoint.passcode+conferenceData.noteskey.type, keep name + iconUri + entryPointsconferenceDatasilently stripped on storageentryPoints+conferenceId, noconferenceSolutionconferenceDatasilently stripped on storageGoogle's Calendar API enforces
conferenceSolution.key.type="addOn"as a privileged claim that requires the calling OAuth client identity to belong to a registered Workspace Marketplace Conference Add-on. Without that registration, anyconferenceDatapayload that asserts the claim is rejected, and any payload that omits the claim is dropped during storage.The realistic salvage is description mode: gog creates the Zoom meeting via Zoom API, then writes the join URL + meeting ID + passcode into the Calendar event description inside HTML-comment markers. The event renders as a clickable Zoom link in every Calendar UI; the trade-off is no native "Join with Zoom" conference card.
Changes
New
internal/zoom/package: minimum-viable Zoom Meetings client (S2S OAuth token exchange + cached refresh, POST/users/{userId}/meetings, DELETE/meetings/{meetingId}). Production transport ishttp.DefaultTransport; tests inject ahttp.RoundTripper. NoInsecureSkipVerify.New
gog zoom auth setup/gog zoom auth doctorsubcommands. User-level OAuth scopes (meeting:write,meeting:read,user:read) when available;:admingranular variants when the Marketplace UI exposes only those.Secret storage reuses
internal/secretswith namespaced keys (zoom-account/<alias>/client-secret,zoom-account/<alias>/access-token). Non-secret account metadata lives at~/.config/gogcli/zoom/<alias>.json(dir 0700, file 0600). No second keyring.New
internal/cmd/zoom_description.gohelpers: build, replace, remove, and parse the gog-managed Zoom block in event descriptions. Block format:buildConferenceDataZoom branch returns nil (no conferenceData on the Zoom path);buildZoomConferenceDataremoved. Meet path is unchanged.extractZoomMeetingIDnow parses the description block first and falls back to legacyconferenceDataparsing for events created by the Zoom for Google Workspace add-on or other tools.eventConferenceProviderdetects the gog Zoom block in the description before inspectingconferenceData, so cross-provider mutex checks behave correctly for both shapes.Flag-mutex matrix enforced at parse time for the seven incompatible pairs in calendar_edit.go (
--with-zoom+--regenerate-zoom, etc.). Unchanged.Cross-provider runtime check:
--with-zoomon an event that already has a Meet conference returnsusage("event already has a Meet conference; use --remove-meet first, then --with-zoom"). (--remove-meetitself is out of scope for this PR; see "Deferred".)Cancel-before-create failure semantics for
--regenerate-zoom: cancel old meeting -> on 404/410 proceed; on other error abort, Calendar event unchanged. Create new meeting -> patch Calendar description. If the Calendar patch fails, the newly-created Zoom meeting is cancelled.--remove-zoomcancels the Zoom meeting (404/410 = success, other error = stderr warning + proceed), strips the gog Zoom block from the description, and (for backward compatibility) also clears any legacyconferenceDatathat looks like a Zoom conference.redactZoomURLininternal/zoom/redact.gomasks thepwd=query parameter on every join URL reaching stdout/stderr.--include-passwordsflag (orGOG_ZOOM_INCLUDE_PASSWORDS=1) opts out for debugging. The passcode in the description block is intentionally rendered as the standalonePasscode: <pwd>line.Every destructive Zoom API call writes a structured audit line to stderr:
[zoom] meeting=<id> action=<cancel|delete|regenerate> ts=<rfc3339> cmd=<argv0>.conferenceDataVersion=1is no longer sent for--with-zoom(description-mode does not mutateconferenceData); still sent for--with-meetand for--remove-zoomwhen a legacyconferenceDatafield is being cleared.Testing
internal/cmd/calendar_zoom_test.goandinternal/cmd/calendar_create_update_test.go, all 16 Zoom-specific tests pass.GOGCLI_DEBUG_MODEandGOGCLI_DEBUG_SCRIPT_IDenv vars on the binary; the experimental gates were removed before this commit, but the methodology is recorded for posterity.Deferred to follow-up PRs
To keep this PR close to the project's median size:
gog zoom meeting create|get|list|update|deletesubtree.--remove-meet(real gap, but adjacent to this work).--zoom-no-notesand--zoom-host <email>flags (latter needs the*:adminscope split).gog zoom auth tokenssubcommand.Open Questions answered in #589
Flag naming: chose the concrete
--with-zoomfamily over a generic--conference-provider zoomform. Rationale: discoverability + mirrors the existing--with-meetprecedent. Happy to refactor if you prefer the abstraction.Implementation architecture: direct Zoom API write attached to the event description (path a in the issue, after the architectural revision documented above). Calendar
createRequestpassthrough (path b) was rejected because it depends on third-party add-ons implementing Google's Conference Add-oncreateConferencecallback - Zoom's add-on does not.addOnParametersserver-side validation: empirically tested. Addingparameters.addOnParameters.parameters.scriptIddoes not unlock thekey.type="addOn"write; Google validates the calling client identity rather than the payload contents.